﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static LightCAD.MathLib.Constants;
using LightCAD.Three.OpenGL;
using LightCAD.MathLib;

namespace LightCAD.Three
{
    public class WebGLShadowMap
    {

        private WebGLRenderer _renderer;
        private WebGLObjects _objects;
        private WebGLCapabilities _capabilities;

        private Frustum _frustum;
        private Vector2 _shadowMapSize;
        private Vector2 _viewportSize;
        private Vector4 _viewport;
        private MeshDepthMaterial _depthMaterial;
        private MeshDistanceMaterial _distanceMaterial;
        private JsObj<JsObj<Material>> _materialCache;
        private JsObj<int, int> shadowSide;
        private int _maxTextureSize;

        private ShaderMaterial shadowMaterialVertical;
        private ShaderMaterial shadowMaterialHorizontal;

        private BufferGeometry fullScreenTri;
        private Mesh fullScreenMesh;

        public bool enabled;
        public bool autoUpdate;
        public bool needsUpdate;
        public int type;

        public WebGLShadowMap(WebGLRenderer _renderer, WebGLObjects _objects, WebGLCapabilities _capabilities)
        {
            this._renderer = _renderer;
            this._objects = _objects;
            this._capabilities = _capabilities;

            _frustum = new Frustum();

            _shadowMapSize = new Vector2();
            _viewportSize = new Vector2();

            _viewport = new Vector4();

            _depthMaterial = new MeshDepthMaterial{ depthPacking = RGBADepthPacking };
            _distanceMaterial = new MeshDistanceMaterial();

            _materialCache = new JsObj<JsObj<Material>> { };

            _maxTextureSize = _capabilities.maxTextureSize;

            shadowSide = new JsObj<int, int>
                {
                    { FrontSide, BackSide },
                    { BackSide, FrontSide },
                    { DoubleSide, DoubleSide }
                };
            shadowMaterialVertical = new ShaderMaterial
            {
                defines = new JsObj<object>() {
                        { "VSM_SAMPLES", 8 }
                    },
                uniforms = new Uniforms
                    {
                    {"shadow_pass", new Uniform{ value = null} },
                    {"resolution", new Uniform{ value = new Vector2()} },
                    {"radius", new Uniform{ value = 4.0} },
                    },
                vertexShader = glsl_vsm.vertex,
                fragmentShader = glsl_vsm.fragment
            };
            shadowMaterialHorizontal = shadowMaterialVertical.clone() as ShaderMaterial;
            shadowMaterialHorizontal.defines["HORIZONTAL_PASS"] = "1";

            fullScreenTri = new BufferGeometry();
            fullScreenTri.setAttribute(
                "position",
                new BufferAttribute(
                    new double[] { -1, -1, 0.5, 3, -1, 0.5, -1, 3, 0.5 },
                    3
                )
            );
            fullScreenMesh = new Mesh(fullScreenTri, shadowMaterialVertical);

            this.enabled = false;

            this.autoUpdate = true;
            this.needsUpdate = false;

            this.type = PCFShadowMap;
        }

        public void render(ListEx<Light> lights, Object3D scene, Camera camera)
        {
            //return;
            if (this.enabled == false) return;
            if (this.autoUpdate == false && this.needsUpdate == false) return;

            if (lights.Length == 0) return;

            var currentRenderTarget = _renderer.getRenderTarget();
            var activeCubeFace = _renderer.getActiveCubeFace();
            var activeMipmapLevel = _renderer.getActiveMipmapLevel();

            var _state = _renderer.state;

            // Set GL state for depth map.
            _state.setBlending(NoBlending);
            _state.buffers.color.setClear(1, 1, 1, 1);
            _state.buffers.depth.setTest(true);
            _state.setScissorTest(false);

            // render depth map

            for (int i = 0, il = lights.Length; i < il; i++)
            {

                var light = lights[i];
                var shadow = light.shadow;

                if (shadow == null)
                {

                    console.warn("THREE.WebGLShadowMap:", light, "has no shadow.");
                    continue;

                }

                if (shadow.autoUpdate == false && shadow.needsUpdate == false) continue;

                _shadowMapSize.Copy(shadow.mapSize);

                var shadowFrameExtents = shadow.getFrameExtents();

                _shadowMapSize.Multiply(shadowFrameExtents);

                _viewportSize.Copy(shadow.mapSize);

                if (_shadowMapSize.X > _maxTextureSize || _shadowMapSize.Y > _maxTextureSize)
                {

                    if (_shadowMapSize.X > _maxTextureSize)
                    {

                        _viewportSize.X = Math.Floor(_maxTextureSize / shadowFrameExtents.X);
                        _shadowMapSize.X = _viewportSize.X * shadowFrameExtents.X;
                        shadow.mapSize.X = _viewportSize.X;

                    }

                    if (_shadowMapSize.Y > _maxTextureSize)
                    {

                        _viewportSize.Y = Math.Floor(_maxTextureSize / shadowFrameExtents.Y);
                        _shadowMapSize.Y = _viewportSize.Y * shadowFrameExtents.Y;
                        shadow.mapSize.Y = _viewportSize.Y;

                    }

                }

                if (shadow.map == null)
                {

                    var pars = (this.type != VSMShadowMap) ? new RenderTargetOptions { minFilter = NearestFilter, magFilter = NearestFilter } : null;

                    shadow.map = new WebGLRenderTarget((int)_shadowMapSize.X, (int)_shadowMapSize.Y, pars);
                    shadow.map.texture.name = light.name + ".shadowMap";

                    shadow.camera.updateProjectionMatrix();

                }

                _renderer.setRenderTarget(shadow.map);
                _renderer.clear();

                var viewportCount = shadow.getViewportCount();

                for (int vp = 0; vp < viewportCount; vp++)
                {

                    var viewport = shadow.getViewport(vp);

                    _viewport.Set(
                        _viewportSize.X * viewport.X,
                        _viewportSize.Y * viewport.Y,
                        _viewportSize.X * viewport.Z,
                        _viewportSize.Y * viewport.W
                    );

                    _state.viewport(_viewport);

                    shadow.updateMatrices(light, vp);

                    _frustum = shadow.getFrustum();

                    renderObject(scene, camera, shadow.camera, light, this.type);

                }

                // do blur pass for VSM

                if (shadow is PointLightShadow != true && this.type == VSMShadowMap)
                {

                    VSMPass(shadow, camera);

                }

                shadow.needsUpdate = false;

            }

            this.needsUpdate = false;

            _renderer.setRenderTarget(currentRenderTarget, activeCubeFace, activeMipmapLevel);

        }


        private void VSMPass(LightShadow shadow, Camera camera)
        {

            var geometry = _objects.update(fullScreenMesh);

            if (Convert.ToInt32(shadowMaterialVertical.defines["VSM_SAMPLES"]) != shadow.blurSamples)
            {

                shadowMaterialVertical.defines["VSM_SAMPLES"] = shadow.blurSamples.ToString();
                shadowMaterialHorizontal.defines["VSM_SAMPLES"] = shadow.blurSamples.ToString();

                shadowMaterialVertical.needsUpdate = true;
                shadowMaterialHorizontal.needsUpdate = true;

            }

            if (shadow.mapPass == null)
            {

                shadow.mapPass = new WebGLRenderTarget((int)_shadowMapSize.X, (int)_shadowMapSize.Y);

            }

            // vertical pass

            shadowMaterialVertical.uniforms["shadow_pass"].value = shadow.map.texture;
            shadowMaterialVertical.uniforms["resolution"].value = shadow.mapSize;
            shadowMaterialVertical.uniforms["radius"].value = shadow.radius;
            _renderer.setRenderTarget(shadow.mapPass);
            _renderer.clear();
            _renderer.renderBufferDirect(camera, null, geometry, shadowMaterialVertical, fullScreenMesh, null);

            // horizontal pass

            shadowMaterialHorizontal.uniforms["shadow_pass"].value = shadow.mapPass.texture;
            shadowMaterialHorizontal.uniforms["resolution"].value = shadow.mapSize;
            shadowMaterialHorizontal.uniforms["radius"].value = shadow.radius;
            _renderer.setRenderTarget(shadow.map);
            _renderer.clear();
            _renderer.renderBufferDirect(camera, null, geometry, shadowMaterialHorizontal, fullScreenMesh, null);

        }


        private Material getDepthMaterial(Object3D _object, Material material, Light light, int type)
        {
            Material result = null;
            Material customMaterial = (light is PointLight) ? _object.customDistanceMaterial as Material : _object.customDepthMaterial;

            if (customMaterial != null)
            {
                result = customMaterial;
            }
            else
            {
                if (light is PointLight)
                    result = _distanceMaterial;
                else
                    result = _depthMaterial;

                if ((_renderer.localClippingEnabled && material.clipShadows && material.clippingPlanes != null && material.clippingPlanes.Length != 0) ||
                    (material.displacementMap != null && material.displacementScale != 0) ||
                    (material.alphaMap != null && material.alphaTest > 0) ||
                    (material.map != null && material.alphaTest > 0))
                {

                    // in this case we need a unique material instance reflecting the
                    // appropriate state

                    var keyA = result.uuid;
                    var keyB = material.uuid;

                    var materialsForVariant = _materialCache[keyA];

                    if (materialsForVariant == null)
                    {

                        materialsForVariant = new JsObj<Material>();
                        _materialCache[keyA] = materialsForVariant;

                    }

                    var cachedMaterial = materialsForVariant[keyB];

                    if (cachedMaterial == null)
                    {

                        cachedMaterial = result.clone();
                        materialsForVariant[keyB] = cachedMaterial;

                    }

                    result = cachedMaterial;

                }

            }

            result.visible = material.visible;
            result.wireframe = material.wireframe;

            if (type == VSMShadowMap)
            {

                result.side = (material.shadowSide != 0) ? material.shadowSide : material.side;

            }
            else
            {

                result.side = (material.shadowSide != 0) ? material.shadowSide : shadowSide[material.side];

            }


            result.alphaMap = material.alphaMap;
            result.alphaTest = material.alphaTest;
            result.map = material.map;

            result.clipShadows = material.clipShadows;
            result.clippingPlanes = material.clippingPlanes;
            result.clipIntersection = material.clipIntersection;

            result.displacementMap = material.displacementMap;
            result.displacementScale = material.displacementScale;
            result.displacementBias = material.displacementBias;

            result.wireframeLinewidth = material.wireframeLinewidth;
            result.linewidth = material.linewidth;

            if (light is PointLight && result is MeshDistanceMaterial)
            {
                var materialProperties = _renderer.properties.get(result);
                materialProperties.light = light;
            }

            return result;

        }

        private void renderObject(Object3D _object, Camera camera, Camera shadowCamera, Light light, int type)
        {

            if (_object.visible == false) return;

            var visible = _object.layers.test(camera.layers);

            if (visible && (_object is Mesh || _object is Line || _object is Points))
            {

                if ((_object.castShadow || (_object.receiveShadow && type == VSMShadowMap)) && (!_object.frustumCulled || _frustum.IntersectsObject(_object)))
                {

                    _object.modelViewMatrix.MultiplyMatrices(shadowCamera.matrixWorldInverse, _object.matrixWorld);

                    var geometry = _objects.update(_object);

                    ListEx<Material> materials = null;
                    if (_object is Mesh)
                    {
                        materials = (_object as Mesh).materials;
                    }
                    else if (_object is Line)
                    {
                        materials = (_object as Line).materials;
                    }
                    else if (_object is Points)
                    {
                        materials = new ListEx<Material>() { (_object as Points).material };
                    }
                    var material = materials[0];

                    if (materials.Length > 1)
                    {

                        var groups = geometry.groups;

                        for (int k = 0, kl = groups.Length; k < kl; k++)
                        {

                            var group = groups[k];
                            var groupMaterial = materials[group.MaterialIndex];

                            if (groupMaterial != null && groupMaterial.visible)
                            {

                                var depthMaterial = getDepthMaterial(_object, groupMaterial, light, type);

                                _renderer.renderBufferDirect(shadowCamera, null, geometry, depthMaterial, _object, group);

                            }

                        }

                    }
                    else if (material.visible)
                    {

                        var depthMaterial = getDepthMaterial(_object, material, light, type);

                        _renderer.renderBufferDirect(shadowCamera, null, geometry, depthMaterial, _object, null);

                    }

                }

            }

            var children = _object.children;

            for (int i = 0, l = children.Length; i < l; i++)
            {

                renderObject(children[i], camera, shadowCamera, light, type);

            }

        }
    }
}
